import json
from dataclasses import is_dataclass
from enum import Enum
from typing import List, Any, Generator, Iterator, get_origin, Literal, get_args, Union, Tuple

from .typing import is_named_tuple, is_optional_type


class TokenType(Enum):
    EOL = 0
    INDENT = 1
    UNINDENT = -1
    COM = 2
    DOC = 3
    SYM = 4
    KEY = 5
    TYPE = 6
    CONST = 7


_TYPE_TO_TS = {
    Any:        [(TokenType.TYPE, "any")],
    type(None): [(TokenType.CONST, "null")],
    int:        [(TokenType.TYPE, "number")],
    float:      [(TokenType.TYPE, "number")],
    str:        [(TokenType.TYPE, "string")],
    bool:       [(TokenType.TYPE, "boolean")],
    object:     [(TokenType.SYM, "{"), (TokenType.SYM, "}")]
}


def _gen_comments(cls: Any) -> Iterator[Tuple[TokenType, str]]:
    return []


def to_typescript_tokens(cls: Any, no_except=True) -> Iterator[Tuple[TokenType, str]]:
    if cls in _TYPE_TO_TS:
        yield from _TYPE_TO_TS[cls]
        return

    if cls is None:
        yield TokenType.CONST, "null"
        return

    if cls is dict:
        yield TokenType.SYM, "{"
        yield TokenType.SYM, "["
        yield TokenType.KEY, "key"
        yield TokenType.SYM, ": "
        yield TokenType.TYPE, "string"
        yield TokenType.SYM, "]"
        yield TokenType.SYM, ": "
        yield TokenType.TYPE, "any"
        yield TokenType.SYM, "}"
        return None

    if cls is list:
        yield TokenType.TYPE, "any"
        yield TokenType.SYM, "["
        yield TokenType.SYM, "]"
        return None

    origin_type = get_origin(cls)
    if origin_type is Literal:
        enum_values = get_args(cls)

        for i, enum_value in enumerate(enum_values):
            if i > 0: yield TokenType.SYM, " | "
            yield TokenType.TYPE, json.dumps(enum_value)
        return

    if origin_type is tuple:
        sub_types = get_args(cls)

        yield TokenType.SYM, "["
        for i, sub_type in enumerate(sub_types):
            if i > 0: yield TokenType.SYM, ", "
            yield from to_typescript_tokens(sub_type)
        yield TokenType.SYM, "]"
        return

    if origin_type is list:
        sub_type, = get_args(cls)
        multiline = get_origin(sub_type) is list

        yield TokenType.TYPE, "Array"
        yield TokenType.SYM, "<"
        if multiline:
            yield TokenType.EOL, ""
            yield TokenType.INDENT, ""
        yield from to_typescript_tokens(sub_type)
        if multiline:
            yield TokenType.EOL, ""
            yield TokenType.UNINDENT, ""
        yield TokenType.SYM, ">"
        return

    if origin_type is dict:
        key_type, value_type = get_args(cls)
        yield TokenType.SYM, "{"
        yield TokenType.EOL, ""
        yield TokenType.INDENT, ""
        yield TokenType.SYM, "["
        yield TokenType.KEY, "key"
        yield TokenType.SYM, ": "
        yield TokenType.TYPE, "string"
        yield TokenType.SYM, "]"
        yield TokenType.SYM, ": "
        yield from to_typescript_tokens(value_type)
        yield TokenType.EOL, ""
        yield TokenType.UNINDENT, ""
        yield TokenType.SYM, "}"
        return

    if origin_type is Union:
        sub_types = get_args(cls)
        multiline = any(get_origin(c) is dict for c in sub_types)
        if multiline:
            yield TokenType.EOL, ""
            yield TokenType.INDENT, ""
        for i, sub_type in enumerate(sub_types):
            if multiline:
                yield TokenType.EOL, ""
            if i > 0:
                yield TokenType.SYM, " | "
            yield from to_typescript_tokens(sub_type)
        if multiline:
            yield TokenType.EOL, ""
            yield TokenType.UNINDENT, ""
        return

    if hasattr(cls, "__to_typescript_tokens__"):
        yield from cls.__to_typescript_tokens__()
        return

    if is_dataclass(cls) or is_named_tuple(cls):
        result = {}
        for base in cls.__bases__:
            if base in [object, tuple]:
                continue
            for key, anno in getattr(base, "__annotations__", {}).items():
                result[key] = anno
        for key, anno in getattr(cls, "__annotations__", {}).items():
            result[key] = anno

        yield TokenType.SYM, "{"
        yield TokenType.EOL, ""
        yield TokenType.INDENT, ""

        for key, value_type in result.items():
            yield TokenType.KEY, key
            if is_optional_type(value_type):
                yield TokenType.SYM, "?"
                yield TokenType.SYM, ": "
                sub_type, none_type = get_args(value_type)
                yield from to_typescript_tokens(sub_type)
            else:
                yield TokenType.SYM, ": "
                yield from to_typescript_tokens(value_type)
            yield TokenType.EOL, ""

        yield TokenType.UNINDENT, ""
        yield TokenType.SYM, "}"
        return

    if no_except:
        yield TokenType.TYPE, "<Unknown Type>"
    else:
        raise ValueError("unknown type")


def to_typescript_plain_lines(cls) -> List[Tuple[int, str]]:
    lines = []
    line = ""
    indent = 0
    for tk, text in to_typescript_tokens(cls):
        if tk is TokenType.INDENT:
            indent += 1
        elif tk is TokenType.UNINDENT:
            indent -= 1
        elif tk is TokenType.EOL:
            lines.append((indent, line))
            line = ""
        elif tk is TokenType.DOC:
            line += f'/** {text} */'
        elif tk is TokenType.COM:
            line += f'/* {text} */'
        else:
            line += text
    else:
        lines.append((indent, line))
        line = ""
    return lines


def to_typescript_plain_text(cls) -> str:
    s = ""
    for ind, line in to_typescript_plain_lines(cls):
        s += " " * (4 * ind) + line + "\n"
    return s